﻿using EasyCore.App;
using EasyCore.Authentications.Dtos;
using EasyCore.Extensions;
using IdentityModel;
using Microsoft.AspNetCore.Authentication.JwtBearer;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Caching.Distributed;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.IdentityModel.Tokens;
using Microsoft.Net.Http.Headers;
using System.IdentityModel.Tokens.Jwt;
using System.Security.Claims;
using System.Security.Cryptography;
using System.Text;


namespace EasyCore.Authentications
{
    public class AuthTokenService : IAuthTokenService
    {
        private const string RefreshTokenIdClaimType = "refresh_token_id";

        private readonly JwtBearerOptions _jwtBearerOptions;
        private readonly JwtOptions _jwtOptions;
        private readonly SigningCredentials _signingCredentials;
        private readonly ILogger<AuthTokenService> _logger;
        private readonly IHttpContextAccessor _httpContextAccessor;

        public AuthTokenService(IOptionsSnapshot<JwtBearerOptions> jwtBearerOptions, SigningCredentials signingCredentials, ILogger<AuthTokenService> logger,
           IHttpContextAccessor httpContextAccessor)
        {
            _jwtBearerOptions = jwtBearerOptions.Get(JwtBearerDefaults.AuthenticationScheme);
            _jwtOptions = EasyApp.Configuration.Get<JwtOptions>(JwtOptions.Name);
            _signingCredentials = signingCredentials;
            _logger = logger;
            _httpContextAccessor = httpContextAccessor;
        }

        /// <summary>
        /// 创建认证令牌
        /// </summary>
        /// <param name="claimsDic"></param>
        /// <returns></returns>
        public async Task<AuthTokenDto> CreateAuthTokenAsync(Dictionary<string, string> claimsDic)
        {
            var result = new AuthTokenDto();
            var (refreshTokenId, refreshToken) = await CreateRefreshTokenAsync(claimsDic); //创建刷新令牌
            result.RefreshToken = refreshToken;
            result.AccessToken = await CreateAccessTokenAsync(refreshTokenId, claimsDic); //创建访问令牌
            // 将Jwt放入Cookie，这样H5就无需在Header中Jwt传递了（主要是省事）
            if (_httpContextAccessor != null && _httpContextAccessor.HttpContext != null)
            {
                _httpContextAccessor.HttpContext.Response.Cookies.Append(HeaderNames.Authorization, result.AccessToken, new CookieOptions
                {
                    // 本地环境，忽略域
                    //Domain = "",
                    HttpOnly = true,
                    IsEssential = true,
                    MaxAge = TimeSpan.FromDays(_jwtOptions.RefreshTokenExpiresDays),
                    Path = "/",
                    SameSite = Microsoft.AspNetCore.Http.SameSiteMode.Lax
                });
            }
            return result;
        }


        /// <summary>
        /// 创建刷新令牌
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="userName"></param>
        /// <param name="claimDic"></param>
        /// <returns></returns>
        private async Task<(string refreshTokenId, string refreshToken)> CreateRefreshTokenAsync(Dictionary<string, string> claimsDic)
        {
            var tokenId = Guid.NewGuid().ToString("N"); //创建刷新令牌唯一标识
            if (claimsDic == null) claimsDic = new Dictionary<string, string>();
            if (claimsDic.ContainsKey("token_id")) claimsDic["token_id"] = tokenId;
            else claimsDic.Add("token_id", tokenId);
            var expire = _jwtOptions.RefreshTokenExpiresDays * 24 * 60;
            var token = CreateJwtToken(claimsDic, DateTime.UtcNow.AddMinutes(expire));
            EasyApp.Cache.Add(tokenId, token, expire * 60);
            return (tokenId, token);
        }

        /// <summary>
        /// 创建访问令牌
        /// </summary>
        /// <param name="refreshTokenId"></param>
        /// <param name="claimsDic"></param>
        /// <returns></returns>
        private async Task<string> CreateAccessTokenAsync(string refreshTokenId, Dictionary<string, string> claimsDic)
        {
            if (string.IsNullOrWhiteSpace(refreshTokenId)) throw new ArgumentNullException(nameof(refreshTokenId));
            if (claimsDic == null) claimsDic = new Dictionary<string, string>();
            if (claimsDic.ContainsKey(RefreshTokenIdClaimType)) claimsDic[RefreshTokenIdClaimType] = refreshTokenId;
            else claimsDic.Add(RefreshTokenIdClaimType, refreshTokenId);
            var token = CreateJwtToken(claimsDic, DateTime.UtcNow.AddMinutes(_jwtOptions.AccessTokenExpiresMinutes));
            return token;
        }

        /// <summary>
        /// 创建令牌
        /// </summary>
        /// <param name="userId"></param>
        /// <param name="userName"></param>
        /// <param name="claimDic"></param>
        /// <param name="refreshTokenId"></param>
        /// <returns></returns>
        /// <exception cref="ArgumentNullException"></exception>
        private string CreateJwtToken(Dictionary<string, string> claimDic, DateTime? expires)
        {
            //加入声明
            List<Claim> claims = new List<Claim>();
            if (claimDic != null && claimDic.Any())
            {
                foreach (var key in claimDic.Keys)
                {
                    claims.Add(new Claim(key, claimDic[key]));
                }
            }

            //获得JWT配置
            var config = EasyApp.Configuration;
            var jwtSettings = config.Get<JwtOptions>(JwtOptions.Name);

            //创建签名证书
            var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKeyStr));
            var creds = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);

            //声明描述器
            var tokenDescriptor = new SecurityTokenDescriptor
            {
                Subject = new ClaimsIdentity(claims),
                Issuer = _jwtOptions.Issuer,
                Audience = _jwtOptions.Audience,
                Expires = expires,
                SigningCredentials = creds,
            };

            //创建访问令牌
            var handler = _jwtBearerOptions.SecurityTokenValidators.OfType<JwtSecurityTokenHandler>().FirstOrDefault() ?? new JwtSecurityTokenHandler();
            var securityToken = handler.CreateJwtSecurityToken(tokenDescriptor); //生成安全令牌            
            var tokenStr = handler.WriteToken(securityToken); //令牌写入并返回令牌字符串
            //var tokenHandler = new JwtSecurityTokenHandler();
            //var token = tokenHandler.CreateToken(tokenDescriptor); //生成token             
            //var tokenStr = handler.WriteToken(token); //token转换成字符串
            return tokenStr;
        }

        /// <summary>
        /// 根据刷新令牌来重新获得访问令牌
        /// </summary>
        /// <param name="token"></param>
        /// <returns></returns>
        /// <exception cref="BadHttpRequestException"></exception>
        public async Task<AuthTokenDto> RefreshAuthTokenAsync(AuthTokenDto token)
        {
            var validationParameters = _jwtBearerOptions.TokenValidationParameters.Clone();
            validationParameters.ValidateLifetime = false;

            var handler = _jwtBearerOptions.SecurityTokenValidators.OfType<JwtSecurityTokenHandler>().FirstOrDefault()
                ?? new JwtSecurityTokenHandler();
            ClaimsPrincipal principal = null;
            try
            {
                principal = handler.ValidateToken(token.AccessToken, validationParameters, out _);
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex.ToString());
                throw new BadHttpRequestException("Invalid access token");
            }

            //获得用户标识
            var identity = principal.Identities.First();
            var refreshTokenId = identity.Claims.FirstOrDefault(c => c.Type == RefreshTokenIdClaimType).Value;

            //验证刷新令牌
            var refreshToken = EasyApp.Cache.Get<string>(refreshTokenId);
            if (refreshToken != token.RefreshToken)
            {
                throw new BadHttpRequestException("Invalid refresh token");
            }

            EasyApp.Cache.Remove(refreshTokenId); //从缓存中去掉
            Dictionary<string, string> claimsDic = new Dictionary<string, string>();
            foreach (var item in identity.Claims)
            {
                claimsDic.Add(item.Type, item.Value);
            }

            //创建令牌并返回
            return await CreateAuthTokenAsync(claimsDic);
        }
    }
}
